/*
 * Copyright (C) 2009 Leandro de Oliveira Aparecido <lehphyro@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.code.japa.engine.impl;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;

import org.apache.commons.lang.*;
import org.slf4j.*;
import org.springframework.beans.*;
import org.springframework.beans.factory.*;
import org.springframework.orm.jpa.*;
import org.springframework.util.*;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.*;

import com.google.code.japa.engine.*;
import com.google.code.japa.repository.collection.*;
import com.google.code.japa.repository.query.*;
import com.google.code.japa.repository.query.loader.*;
import com.google.code.japa.repository.ret.*;

/**
 * Default implementation of the JAPA engine supporting all persistence features.
 * <p>This class is not intended to be used directly. Please see {@link com.google.code.japa.Repository}
 * if you want to use the JPA API with the features that JAPA offers.
 * 
 * @author Leandro Aparecido
 * @since 1.0
 * @see com.google.code.japa.engine.JapaEngine
 * @see com.google.code.japa.Repository
 */
public class DefaultJapaEngine implements JapaEngine, InitializingBean {

	private static final Logger logger = LoggerFactory.getLogger(DefaultJapaEngine.class);
	
	/**
	 * Helper to perform JPA operations.
	 */
	private JpaOperations jpaOperations;
	
	/**
	 * Responsible for executing queries and interpreting their results.
	 */
	private RepositoryQueryLoader repositoryQueryLoader;
	
	/**
	 * Parser of JAPA query annotations.
	 */
	private RepositoryQueryParser repositoryQueryParser;
	
	/**
	 * Parser of JAPA return annotations.
	 */
	private RepositoryReturnParser repositoryReturnParser;
	
	/**
	 * Filter of annotated fields.
	 */
	private FieldFilter repositoryQueryFieldFilter;
	
	/**
	 * Filter of annotated methods. 
	 */
	private MethodFilter repositoryQueryMethodFilter;
	
	/**
	 * Registry to store converters of data types.
	 */
	private PropertyEditorRegistry propertyEditorRegistry;

	/**
	 * Constructs a japa engine using default implementations of annotation parsers, filters and
	 * other support classes.
	 */
	public DefaultJapaEngine() {
		repositoryQueryParser = new RepositoryQueryParser();
		repositoryReturnParser = new RepositoryReturnParser();
		
		repositoryQueryFieldFilter = new RepositoryQueryFieldFilter();
		repositoryQueryMethodFilter = new RepositoryQueryMethodFilter();
		
		propertyEditorRegistry = new PropertyEditorRegistrySupport();
	}

	/**
	 * {@inheritDoc}
	 */
	public void afterPropertiesSet() throws Exception {
		Validate.notNull(getJpaOperations(), "JpaOperations has not been set");
		Validate.notNull(getRepositoryQueryLoader(), "RepositoryQueryLoader has not been set");
	}
	
	/**
	 * {@inheritDoc}
	 */
	public JapaObject processObject(Object obj) {
		List<PersistentCollection<?>> persistentCollections = new ArrayList<PersistentCollection<?>>();
		persistentCollections.addAll(loadFields(obj));
		persistentCollections.addAll(loadMethods(obj));
		
		return new JapaObject(obj, persistentCollections);
	}

	/**
	 * {@inheritDoc}
	 */
	public MethodCallContext processMethodCall(JapaObject obj, Method method, Object... args) {
		LOCAL_JAPA_ENGINE.set(this);
		
		Object[] modifiedArguments = loadArguments(method, args);
		return new MethodCallContext(obj, method, args, modifiedArguments);
	}

	/**
	 * {@inheritDoc}
	 */
	public Object processMethodReturn(MethodCallContext context, Object returnValue) throws Throwable {
		try {
			return loadReturnValue(context.getMethod(), returnValue, context.getModifiedArguments());
		} finally {
			try {
				if (context.getTarget() != null) {
					context.getTarget().flush();
				}
			} finally {
				LOCAL_JAPA_ENGINE.remove();
			}
		}
	}

	/**
	 * {@inheritDoc}
	 */
	@SuppressWarnings("unchecked")
	public <T> List<T> executeQuery(String clause, Object... params) {
		Validate.notNull(getJpaOperations(), "Cannot execute query. JpaOperations has not been set");
		logger.debug("Executing JPA query:\n{}\nwith parameters: {}", clause, Arrays.toString(params));
		return getJpaOperations().find(clause, params);
	}
	
	/**
	 * {@inheritDoc}
	 */
	public <T> T findById(Class<T> entityClass, Object id) {
		Validate.notNull(getJpaOperations(), "Cannot execute query. JpaOperations has not been set");
		logger.debug("Retrieving entity of type [{}] by id [{}]", entityClass, id);
		return getJpaOperations().find(entityClass, id);
	}

	/**
	 * Loads and sets annotated fields of the passed object.
	 * 
	 * @param obj Object to load fields to.
	 * @return List of persistent collections associated with the object.
	 * @see com.google.code.japa.repository.collection.PersistentCollection
	 * @see com.google.code.japa.annotation.RepositoryQuery
	 */
	protected List<? extends PersistentCollection<?>> loadFields(final Object obj) {
		logger.debug("Loading repository values for annotated attributes of object [{}]", obj);

		final List<PersistentCollection<?>> collections = new ArrayList<PersistentCollection<?>>();
		ReflectionUtils.doWithFields(obj.getClass(), new FieldCallback() {
			public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
				RepositoryQueryParameters parameters = getRepositoryQueryParser().parse(field);
				Object value = loadFromRepositoryQuery(parameters, field.getType());

				ReflectionUtils.makeAccessible(field);
				field.set(obj, value);

				// Keep reference to persistent collection flush after method invocation
				if (value instanceof PersistentCollection) {
					collections.add((PersistentCollection<?>)value);
				}
			}
		}, getRepositoryQueryFieldFilter());
		
		return collections;
	}
	
	/**
	 * Loads and invokes annotated methods of the passed object.
	 * 
	 * @param obj Object to load methods.
	 * @return List of persistent collections associated with the object.
	 * @see com.google.code.japa.repository.collection.PersistentCollection
	 * @see com.google.code.japa.annotation.RepositoryQuery
	 */
	protected List<PersistentCollection<?>> loadMethods(final Object obj) {
		logger.debug("Loading repository values for annotated methods of object [{}]", obj);
		
		final List<PersistentCollection<?>> collections = new ArrayList<PersistentCollection<?>>();
		ReflectionUtils.doWithMethods(obj.getClass(), new ReflectionUtils.MethodCallback() {
			public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
				RepositoryQueryParameters parameters = getRepositoryQueryParser().parse(method);
				Object value = loadFromRepositoryQuery(parameters, parameters.getType());
				
				MethodInvoker invoker = new MethodInvoker();
				invoker.setTargetObject(obj);
				invoker.setTargetMethod(method.getName());
				invoker.setArguments(new Object[] { value });
				try {
					invoker.prepare();
					invoker.invoke();
				} catch (Exception e) {
					throw new RuntimeException(e);
				}
				
				// Keep reference to persistent collection flush after method invocation
				if (value instanceof PersistentCollection) {
					collections.add((PersistentCollection<?>)value);
				}
			}
		}, getRepositoryQueryMethodFilter());
		
		return collections;
	}

	/**
	 * Loads annotated parameters of the passed method based on method invocation context.
	 * 
	 * @param method Method to load argument values.
	 * @param arguments Arguments originally passed on method invocation.
	 * @return Array of objects of the same size of <code>arguments</code> with the 
	 * values replaced as configured by JAPA annotations. Parameters not configured to be
	 * repository loaded are put into the same position of the originally passed array.
	 * @see com.google.code.japa.annotation.RepositoryQuery
	 */
	protected Object[] loadArguments(Method method, Object... arguments) {
		Class<?>[] argumentTypes = method.getParameterTypes();
		Object[] modifiedArguments = new Object[arguments.length];
		
		RepositoryQueryParameters[] repositoryQueryParameters = getRepositoryQueryParser().parseMethodParameters(method, arguments);
		for (int i = 0; i < arguments.length; i++) {
			if (repositoryQueryParameters[i] == null) {
				modifiedArguments[i] = arguments[i];
			} else {
				logger.debug("Loading value from repository for argument [{}] for invocation of [{}]", i, method);
				Object value = loadFromRepositoryQuery(repositoryQueryParameters[i], argumentTypes[i]);

				logger.debug("Replacing argument [{}] with repository value [{}] for invocation of [{}]", new Object[] { i, value, method });
				modifiedArguments[i] = value;
			}
		}
		
		return modifiedArguments;
	}
	
	/**
	 * Loads return value of the passed method based on method invocation context.
	 * 
	 * @param method Method to load return value.
	 * @param returnValue Value originally returned by the method invocation.
	 * @param arguments Arguments passed on method invocation.
	 * Note that it should be the array returned by {@link #loadArguments(Method, Object...)}.
	 * @return Loaded return value or original return value if no repository access is configured.
	 * @throws Throwable Configured exception to be thrown if return value met some specific condition
	 * or JPA/JAPA runtime error.
	 * @see com.google.code.japa.annotation.RepositoryReturn
	 */
	@SuppressWarnings("unchecked")
	protected Object loadReturnValue(Method method, Object returnValue, Object... arguments) throws Throwable {
		Object ret;
		
		RepositoryReturnParameters returnParameters = getRepositoryReturnParser().parse(method, arguments, returnValue);
		if (returnParameters == null) {
			ret = returnValue;
		} else {
			ret = loadFromRepositoryQuery(returnParameters, method.getReturnType());
			if (ret == null && returnParameters.shouldThrowException()) {
				throw returnParameters.getEmpty().newInstance();
			} else if (ret instanceof Collection) {
				Collection collection = (Collection)ret;
				if (collection.isEmpty()) {
					if (returnParameters.shouldThrowException()) {
						throw returnParameters.getEmpty().newInstance();
					}
					ret = returnValue;
				}
			}
		}

		return ret;
	}

	/**
	 * Loads a value from the repository executing value conversions if necessary.
	 * 
	 * @param parameters Parameters to execute the repository query.
	 * @param targetClass Class of the object that should be returned.
	 * @return Converted value loaded from repository.
	 * @see com.google.code.japa.repository.query.RepositoryQueryParameters
	 */
	protected Object loadFromRepositoryQuery(RepositoryQueryParameters parameters, Class<?> targetClass) {
		List<Object> list = getRepositoryQueryLoader().load(parameters);
		
		// Extract value from result if the argument is not a collection
		Object valueToReturn;
		if (Collection.class.isAssignableFrom(targetClass)) {
			valueToReturn = list;
		} else {
			Object valueToConvert;
			if (list instanceof PersistentCollection) {
				valueToConvert = list;
			} else if (list.size() > 1) {
				String message = "Loaded more than one object to set non-collection value.";
				if (parameters.isExtent()) {
					message += " Are you sure the [" + parameters.getTargetType() + "] should receive an extent?";
				} else {
					message += " Are you sure the query [" + parameters.getQueryName() + "] should return only one value?";
				}
				throw new IllegalArgumentException(message);
			} else if (list.isEmpty()) {
				valueToConvert = null;
			} else {
				valueToConvert = list.get(0);
			}
			
			// No need to convert persistent collection
			if (list instanceof PersistentCollection) {
				valueToReturn = valueToConvert;
			} else {
				PropertyEditor editor = getPropertyEditorRegistry().findCustomEditor(targetClass, null);
				if (editor == null) {
					logger.debug("No converter found for class [{}], trying to set without conversion", targetClass.getName());
					// Try to set using no conversion
					valueToReturn = valueToConvert;
				} else {
					if (valueToConvert == null) {
						logger.debug("Converting null value to type [{}]", targetClass.getName());
					} else {
						logger.debug("Converting value [{}] of type [{}] to type [{}]", new Object[] { valueToConvert, valueToConvert.getClass().getName(), targetClass });
					}
					editor.setValue(valueToConvert);
					valueToReturn = editor.getValue();
				}
			}
		}
		
		return valueToReturn;
	}
	
	// Getters/Setters

	public JpaOperations getJpaOperations() {
		return jpaOperations;
	}
	
	public void setJpaOperations(JpaOperations jpaOperations) {
		this.jpaOperations = jpaOperations;
	}
	
	public RepositoryQueryLoader getRepositoryQueryLoader() {
		return repositoryQueryLoader;
	}

	public void setRepositoryQueryLoader(RepositoryQueryLoader repositoryQueryLoader) {
		this.repositoryQueryLoader = repositoryQueryLoader;
	}

	public RepositoryQueryParser getRepositoryQueryParser() {
		return repositoryQueryParser;
	}

	public void setRepositoryQueryParser(RepositoryQueryParser repositoryQueryParser) {
		this.repositoryQueryParser = repositoryQueryParser;
	}

	public RepositoryReturnParser getRepositoryReturnParser() {
		return repositoryReturnParser;
	}

	public void setRepositoryReturnParser(RepositoryReturnParser repositoryReturnParser) {
		this.repositoryReturnParser = repositoryReturnParser;
	}

	public FieldFilter getRepositoryQueryFieldFilter() {
		return repositoryQueryFieldFilter;
	}

	public void setRepositoryQueryFieldFilter(FieldFilter repositoryQueryFieldFilter) {
		this.repositoryQueryFieldFilter = repositoryQueryFieldFilter;
	}

	public MethodFilter getRepositoryQueryMethodFilter() {
		return repositoryQueryMethodFilter;
	}

	public void setRepositoryQueryMethodFilter(MethodFilter repositoryQueryMethodFilter) {
		this.repositoryQueryMethodFilter = repositoryQueryMethodFilter;
	}
	
	public PropertyEditorRegistry getPropertyEditorRegistry() {
		return propertyEditorRegistry;
	}
	
	public void setPropertyEditorRegistry(PropertyEditorRegistry propertyEditorRegistry) {
		this.propertyEditorRegistry = propertyEditorRegistry;
	}
}
